﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using DomainDriver.CommonUtilities.Reflectors;
using DomainDriver.DomainModeling.DomainModels;
using DomainDriver.DomainModeling.DomainObjects;
using DomainDriver.DomainModeling.Events;
using DomainDriver.DomainModeling.Repositories;
using DomainDriver.DomainModeling.Queries;

namespace DomainDriver.DomainModeling.Constraints
{
    public class ParentChildConstraint<PARENT_KEY, CHILD_KEY, PARENT, CHILD> : ConstraintBase
        where PARENT : IKeyedDomainObject<PARENT_KEY, PARENT>
        where CHILD : IKeyedDomainObject<CHILD_KEY, CHILD>
    {
        private string m_OriginalPropertyName;
        private List<PropertyInfo> m_ParentIdPropertyInfos;

        /// <summary>
        /// Use like "(CHILD dObj) => (PARENT_KEY)dObj.Parent" where cast is necessary if property is nullable.
        /// </summary>
        /// <param name="domainModel"></param>
        /// <param name="propertyGetter"></param>
        public ParentChildConstraint(IDomainModel domainModel, Expression<Func<CHILD, PARENT_KEY>> propertyGetter)
            : this(domainModel, ClassReflector.GetPropertyName<CHILD, PARENT_KEY>(propertyGetter))
        { }

        public ParentChildConstraint(IDomainModel domainModel, string parentIdPropertyNameOfChild)
            : base(domainModel)
        {
            m_ParentIdPropertyInfos = ExtractConstrainedPropertyInfo<CHILD>(parentIdPropertyNameOfChild);
            if (m_ParentIdPropertyInfos == null)
            { throw new ApplicationException(); }
            m_OriginalPropertyName = parentIdPropertyNameOfChild;

            ChildRepository.AddingDomainObject += new AddingDomainObjectDelegate<CHILD_KEY, CHILD>(ChildRepository_AddingDomainObject);
            ChildRepository.UpdatingDomainObject += new UpdatingDomainObjectDelegate<CHILD_KEY, CHILD>(ChildRepository_UpdatingDomainObject);
            ParentRepository.RemovingDomainObject += new RemovingDomainObjectDelegate<PARENT_KEY, PARENT>(ParentRepository_RemovingDomainObject);
        }

        protected IRepository<CHILD_KEY, CHILD> ChildRepository
        {
            get { return DomainModel.GetRepository<CHILD_KEY, CHILD>(); }
        }

        protected IRepository<PARENT_KEY, PARENT> ParentRepository
        {
            get { return DomainModel.GetRepository<PARENT_KEY, PARENT>(); }
        }

        protected object GetPropertyValueForChildInstance(CHILD childInstance)
        {
            return ClassReflector.GetNestedPropertyValue(childInstance, m_ParentIdPropertyInfos);
        }

        protected bool IsChildChangeValid(CHILD child)
        {
            if (!Enabled)
            { return true; }

            object keyToVerify = GetPropertyValueForChildInstance(child);

            if (keyToVerify == null)
            { return true; }

            PARENT_KEY parentKey = (PARENT_KEY)keyToVerify;
            if (ParentRepository.Exists(parentKey))
            { return true; }

            return false;
        }

        protected bool IsParentDeleteValid(PARENT parent)
        {
            if (!Enabled)
            { return true; }

            string parameterName = m_OriginalPropertyName;
            object parameterValue = parent.Key;
            EqualsQuery<CHILD> queryForMatchingChildren = new EqualsQuery<CHILD>(parameterName, parameterValue);

            ICollection<CHILD_KEY> matchingChildKeys = ChildRepository.FindKeys(queryForMatchingChildren);
            if (matchingChildKeys.Count > 0)
            { return false; }

            return true;
        }

        protected void ChildRepository_AddingDomainObject(IRepository<CHILD_KEY, CHILD> sender, AddDomainObjectEventArgs<CHILD_KEY, CHILD> args)
        {
            if (!IsChildChangeValid(args.ObjectToAdd))
            {
                throw new ApplicationException();
            }
        }

        protected void ChildRepository_UpdatingDomainObject(IRepository<CHILD_KEY, CHILD> sender, UpdateDomainObjectEventArgs<CHILD_KEY, CHILD> args)
        {
            if (!IsChildChangeValid(args.UpdatedObject))
            {
                throw new ApplicationException();
            }
        }

        protected void ParentRepository_RemovingDomainObject(IRepository<PARENT_KEY, PARENT> sender, RemoveDomainObjectEventArgs<PARENT_KEY, PARENT> args)
        {
            if (!IsParentDeleteValid(args.ObjectToRemove))
            {
                throw new ApplicationException();
            }
        }
    }
}